iT邦幫忙

2024 iThome 鐵人賽

DAY 3
1

https://ithelp.ithome.com.tw/upload/images/20240917/20168201oy2eV5lHWV.png

今天要來介紹 Module 模式,Module 模式是 GoF 提出的模式之一,我會以 JavaScript 為主要程式語言來說明以及舉例,並盡量以情境(context)、問題(problem)、權衡(force)、解決方案(solution)等描述模式的要素來說明~

情境

隨著專案與程式碼的擴大,程式碼的組織性與可維護性越來越重要,需要一種方法來組織、封裝程式碼及功能,降低功能間的依賴性,並保持全域作用域的乾淨整潔,避免全域作用域被污染。

問題

沒有封裝過的 JavaScript 程式碼很容易造成變數命名衝突,變數的值也很容易被修改,因為所有變數和函數都在全域作用域中,很難保持程式碼隱私性與結構性,也讓程式碼難以維護和預測。
如何封裝程式碼,將不必要的私有變數隱藏,並公開外部能存取的變數和函式?
(備註:其實問題不限於 JavaScript 程式碼,應該說所有程式語言都有類似這樣的問題,只是我們都以 JavaScript 來做說明和舉例)

權衡

  • 封裝需求
    • 安全性:變數和函式應受到保護,以防止外部程式碼的修改,導致應用程式行為難以預測
    • 接口清晰:應提供清晰的公開接口,讓其他外部程式可以與其互動,同時隱藏內部實現細節,減少程式碼間的依賴性
  • 最小化相互作用
    • 降低耦合度:不同功能程式碼之間的直接互動應盡量減少,以降低程式碼之間的耦合,低耦合度可讓程式碼獨立於其他部分,方便修改,有助於長期維護
    • 增加內聚性:封裝在一起的程式碼應共同完成一個明確目標,高內聚可確保整組程式碼的目標清晰,更容易理解和測試
  • 公開接口與私有實現的分離
    • 靈活性:只公開必要的接口,而內部實現則可自由修改,也不影響有依賴此功能的其他程式
    • 易於維護:隱藏內部實現,其他開發者只需關注有公開的部分,更容易使用與維護

解決方案需平衡上述權衡。例如,過度封裝可能會導致程式碼的公開接口不夠靈活,而過度最小化相互作用則可能導致過度抽象,而導致外部難以使用。

解決方案

JavaScript 的模組(module)可解決此問題,ES2015(ES6)之後 JavaScript 有了內建的模組功能,訂定 importexport 的模組語法,我們可用模組來組織物件、函式…等,以便輕鬆匯入和匯出,接下來稍微介紹一下如何使用模組。

模組使用方式

若要使用模組,可分為兩種情境:

  1. 在瀏覽器使用,引入 script 時指定要使用模組類型
    • <script> 中用 type 屬性來指定要使用模組
    • nomodule 則是告訴瀏覽器不須將腳本作為模組載入,對不使用模組語法的 fallback 腳本很有用
    • 載入的 module 檔案路徑一定要寫完整的路徑名稱與副檔名,不能省略 .js
    <script  type='module' src='main.mjs'></script>
    <script  nomodule src='fallback.js'></script>
    
    如果有使用框架來開發前端應用(例如:React),通常都會使用 transpiler (如:Babel)來轉譯模組語法,因此我們可以在應用程式中直接使用 importexport 的語法。
  2. 在 Node.js 使用
    若要使用模組功能,可選其中一種方式設定,也可併用
    • 若希望整個專案都能使用模組,在專案的 package.json 加上 { "type": "module" }
    • 若希望單一檔案使用,就把該檔案的副檔名改成 .mjs

補充一點,.mjs 是用於 JavaScript 模組的副檔名,用來區分模組和一般腳本(.js),.mjs 這個副檔名可讓 runtime 和建構工具(如:Node.js、Babel)將其解析為模組。

模組語法

匯出

如果希望某些變數或函式可以讓外部其他地方使用,就需要使用匯出語法匯出模組,外部才能存取,而沒有使用匯出 export 語法的變數,外部是無法存取的,也因此可保有內部實現的隱私。
匯出方式可分為實名匯出(named export)和預設匯出(default export)。

  • 實名匯出(named export)
    • 一個檔案中可以有多個實名匯出
    // 私人變數,不使用 export 
    const privateNumber = 0;
    
    // 直接在要匯出的變數前面加上 export
    export const userName = 'Foo';
    export const price = 100;
    export function logUserName(userName) {
      console.log('userName is ', userName);
    }
    
    // 私人變數,不使用 export 
    const privateNumber = 0;
    
    // 先定義好變數,在檔案最後再 export 要匯出的變數
    const userName = 'Foo';
    const price = 100;
    function logUserName(userName) {
      console.log('userName is ', userName);
    }
    
    // 統一匯出,但這裡的 {} 並不是物件的意思
    export { userName, price, logUserName}
    
  • 預設匯出(default export)
    • 一個檔案中只能有一個 default export,通常不建議同時使用預設匯出又使用實名匯出
    // 私人變數,不使用 export 
    const privateNumber = 0;
    
    function logUserName(userName) {
      console.log('userName is ', userName);
    }
    
    // 用 export default 來表示這是預設匯出
    export default logUserName
    

匯入

匯入方式會依匯出方式而定,因此也分為兩種。

  • 如果是用實名匯出,匯入時要指名要匯入哪個變數
    import { userName, price, logUserName} from './utils';
    
    • 匯入的時候也可以用 as 來幫變數重新命名
      import { userName as name} from './utils';
      
  • 如果是用預設匯出,匯入時可以自己隨意取名
    import showUserName from './utils'; // 這裡匯入的是我剛剛 export default 的 logUserName,我自己再命名為 showUserName
    

如果沒有使用 export 語法匯出變數,在其他檔案是無法 import 使用的,例如上面範例的 privateNumber,如果要在其他地方 import 存取,會發生錯誤。
另外補充,匯入的模組都是存取同一個參考,因此若要修改匯入的變數,要謹慎~因為其他有匯入此變數的地方也都會被影響,因為模組是 singleton 的,關於 singleton 會在之後文章介紹。

接下來稍微補充匯入的類型。

從遠端來源載入的模組

除了匯入我們在自己專案內定義的模組,也可以匯入遠端模組,例如第三方定義的函式庫。

// 從外部位置載入模組
import { logUserName } from 'https://javascriptpattern.com/modules/utils.js';

logUserName('Foo');

靜態匯入與動態匯入

前面提到的匯入方式都屬於靜態匯入(static import),在主要程式碼之前,會需要預先載入程式碼,這也會導致關鍵功能被延後執行,相對於靜態匯入,另一種是動態匯入(dynamic import)。
動態匯入是需要時再載入,import(url) 會回傳請求模組的 Promise,Promise 成功後再使用模組功能。

const btn = document.getElementById('btn');
btn.addEventListener('click', e => {
    import('/modules/utils.js')
      .then((module)=>{
          // 請求成功後就可用模組的功能
          const { logUserName } = module; // 假設 logUserName 是用實名匯出
          logUserName('Foo');
      })
})

// 或使用 async/await 匯入
let module = await import('/modules/utils.js')   

互動匯入(import on interaction)和可見性匯入(import on visibility)都可利用動態匯入的功能來達成。互動匯入指的是在使用者和網頁特定功能互動時才匯入,例如上方例子,當使用者點擊按鈕時,才動態匯入對應函式庫;可見性匯入指的是在使用者往下捲動頁面時,當頁面可看見該元件時再動態匯入,可搭配 IntersectionObserver API 偵測元件可見時機,再動態載入模組。

優點

以模組作為解決方案的優點如下:

  • 封裝程式碼,可支援私有資料
    • 外部只能存取 export 匯出的資料
    • 可避免汙染全域作用域,也避免命名衝突
  • 可重用性高,可在整個應用的不同地方重複使用

缺點

以模組作為解決方案的缺點如下:

  • 應用程式會以不同方式存取公共或私有成員,若要更改可見性,需更改每個有用到的地方
    • 舉例來說,如果某個函式要從有匯出的公有方法改成不匯出的私有方法,則整個應用中,有存取該方法的地方都要修改
  • 無法輕易擴展私有成員,因為私有變數與邏輯只有模組內可調整,若要擴展或修改較有難度,私有成員較缺乏彈性
  • 無法修補(patch)私有成員,只能覆寫所有與有問題私有成員互動的公有方法

和實際應用的關聯 & 其他補充

這部分不是描述模式的要素之一,如果要說的話應該屬於 known use(?之類的,是一些我想到這模式在目前應用程式如何被使用的小小補充~
模組在現在複雜的前端應用幾乎是不可或缺的,其實幾乎每天開發都會用到😆,以我自己是用 React 開發來說,當我撰寫一個客製化元件要讓外部其他程式碼可以存取時,我就會需要 export 我的元件,然後在需要的地方 import 該元件;又或是我要使用 React 官方提供的 hooks 時,我會需要 import { useState } from 'react',匯入後就可以使用這個 hook 功能,而 React 也可以透過模組的方式來選擇要公開哪些接口讓我們存取,以及要隱藏哪些狀態管理的實作細節,讓我們無法直接存取 React 內部的狀態資料等,因此模組幾乎是我們天天都在用的呀!只是以前沒有這麼清楚知道模組要解決的問題、以及帶來的優缺點等等,希望透過這次機會也讓大家更認識模組模式~

Reference


上一篇
[Day 02] 設計模式簡介
下一篇
[Day 04] Revealing Module 模式
系列文
30天的 JavaScript 設計模式之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言